package edu.utexas.esel.jgraphpad.archpadplugin.persistence;

import java.beans.DefaultPersistenceDelegate;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Stack;

import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.jgraph.JGraph;
import org.jgraph.graph.DefaultEdge;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.DefaultGraphModel;
import org.jgraph.graph.DefaultPort;
import org.jgraph.graph.GraphLayoutCache;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import com.jgraph.editor.JGraphEditorFile;
import com.jgraph.editor.persistence.JGraphPersistenceManagerFactory;
import com.jgraph.editor.persistence.JGraphPersistenceManagerFactoryMethod;
import com.jgraph.pad.JGraphpadDiagram;
import com.jgraph.pad.JGraphpadFile;
import com.jgraph.pad.graph.JGraphpadGraphModel;
import com.jgraph.pad.persistence.JGraphpadAbstractPersistenceManager;
import com.jgraph.pad.persistence.JGraphpadBeanXMLPersistenceManager;
import com.jgraph.pad.util.JGraphpadFocusManager;
import com.sun.xml.internal.bind.util.AttributesImpl;

import edu.utexas.esel.jgraphpad.archpadplugin.ArchPadPlugin;
import edu.utexas.esel.jgraphpad.archpadplugin.Utils;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ArchPadVertexRenderer;
import edu.utexas.esel.jgraphpad.archpadplugin.graph.ElementGraphCell;
import edu.utexas.esel.jgraphpad.archpadplugin.model.ArchPadDiagram;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element;
import edu.utexas.esel.jgraphpad.archpadplugin.model.ElementTreeNode;
import edu.utexas.esel.jgraphpad.archpadplugin.model.Element.ElementDeclaration;

/**
 * This class manages persistence of ArchPad model documents. An archPad model
 * is saved in two files. The first file contains meta data related to its
 * visual presentation in this application. Such information is coordinates
 * of graph cells, border color, etc. This file is generated through the
 * Java XML persistence mechanism and in general is a Java program that
 * encodes how to restore the persisted instance. The second file contains the
 * software architecture model and is in XML format. As opposed to the meta
 * data file this one is not specific for this application and fully describes
 * the model so it can be shared with other applications.
 * <p>
 * Model and meta data files have the same base name suffixed with specific
 * extension. Follows a list of types and corresponding extensions:
 * <p>
 * Model files (not compressed) - basename.archpad.xml
 * Model files (compressed) - basename.archpad.xml.gz
 * Meta data files (not compressed) - basename.xml
 * Meta data files (compressed) - basename.xml.gz 
 *
 * @author svetoslavganov@mail.utexas.edu
 *
 */
public class ArchPadXMLPersistenceManager
        extends JGraphpadBeanXMLPersistenceManager{

	/**
	 * Default ArchPad XML encoding file type (used as a suffix).
	 */
	public static final String TYPE_ARCHPAD_XML = "archpad.xml";

	/**
	 * Default ArchPad XML encoding file type - compressed  (used as a suffix).
	 */
	public static final String TYPE_ARCHPAD_XML_COMPRESSED = "archpad.xml.gz";

	/**
	 * All supported file types by this persistence manager.
	 */
	public static final String[] SUPPORTED_FILE_TYPES = new String[] {
		    TYPE_ARCHPAD_XML,
		    TYPE_ARCHPAD_XML_COMPRESSED
	    };

	private static final String DOCUMENT = "document";

	private static final String MODEL = "model";

	private static final String COMPONENT =
		ArchPadPlugin.TYPE_DECLARATION_COMPONENT.getName();

	private static final String CONNECTOR =
		ArchPadPlugin.TYPE_DECLARATION_CONNECTOR.getName();

	private static final String REGION =
		ArchPadPlugin.TYPE_DECLARATION_REGION.getName();

	private static final String MESSAGE_DATA_AND_METADATA_MISMATCH =
		"Data and metadata mismatch";

	private String mSchemaURI;

	/**
	 * Creates a new instance configured via the provided {@link Properties}.
	 *
	 * @param properties The configuration properties.
	 */
	public ArchPadXMLPersistenceManager(Properties properties) {
		mSchemaURI = properties.getProperty(
				Element.NAME_SCHEMA_LOCATION);

		// we want to save the model in ArchPad specific way
	    removePersistenceDelegate(DefaultGraphCell.class);

		// Adds default persistence delegates
		addPersistenceDelegate(ArchPadDiagram.class,
				new DefaultPersistenceDelegate(new String[] { "name",
						"graphLayoutCache", "properties" }));
	}

	/**
	 * Gets the URI for the XSD schema that defines the software
	 * architecture model.
	 *
	 * @return The schema URI.
	 */
	public String getSchemaURI() {
	    return mSchemaURI;
	}

	public void setSchemaURI(String uri) {
	    mSchemaURI = uri;
	}

	@Override
	public JGraphEditorFile readObject(String uri)
			throws MalformedURLException, IOException {
		JGraphpadFile file = readMetaData(uri);
		return (JGraphpadFile) readData(file, uri);
	}

	/**
	 * Reads the meta data file via delegation to the super class implementation
	 * of {@link JGraphpadBeanXMLPersistenceManager#readObject(String)}.
	 *
	 * @param uri The ArchPad file URI.
	 * @return The read {@link JGraphpadFile}.
	 * @throws IOException If an error occurs while loading the meta data file.
	 */
	private JGraphpadFile readMetaData(String uri) throws IOException {
		String ext = JGraphPersistenceManagerFactory.getFileExtension(uri);
		String currentURI = uri.substring(0, uri.indexOf(ext))
		    + JGraphpadBeanXMLPersistenceManager.TYPE_JAVA_BEAN_XML;
		return (JGraphpadFile) super.readObject(currentURI);
	}

	/**
	 * Read an ArchPad model data. The read data is populated into an
	 * already loaded {@link JGraphpadFile} that contains meta data
	 * used for visual presentation of the graph that represents the model.
	 *
	 * @param file The {@link JGraphpadFile} with meta data. 
	 * @param uri The URI of the data file.
	 * @return The populated with model data file.
	 * @throws IOException If an error occurs while reading the data file.
	 */
	private JGraphEditorFile readData(JGraphpadFile file, String uri)
	    throws IOException {

		// we populate the diagrams of the file with the model data
		List<ArchPadDiagram> diagrams = getDiagrams(file);
        if (diagrams.isEmpty()) {
        	return file;
        }

        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        factory.setValidating(true); 

        try {
            SAXParser parser = factory.newSAXParser();
            factory.setSchema(loadSchema());
    	    SAXParserHandler handler = new SAXParserHandler(diagrams);
            parser.parse(getInputStream(uri), handler);

            return file;
        } catch (SAXException se) {
      	    throw new IOException(se);
        } catch (ParserConfigurationException pce) {
      	    throw new IOException(pce);
        }
	}

	/**
	 * Returns the {@link ArchPadDiagram}s of an {@link JGraphpadFile}.
	 *
	 * @param file The file.
	 * @return The diagrams.
	 */
	private List<ArchPadDiagram> getDiagrams(JGraphpadFile file) {
		List<ArchPadDiagram> diagrams = new ArrayList<ArchPadDiagram>();
		Enumeration<?> children = file.breadthFirstEnumeration();

		while (children.hasMoreElements()) {
			Object child = children.nextElement();
			if (child instanceof JGraphpadDiagram) {
				diagrams.add((ArchPadDiagram) child);
			}
		}

		return diagrams;
	}

	@Override
	public void writeObject(JGraphEditorFile file, String uri)
			throws MalformedURLException, IOException {
		writeMetaData(file, uri);
	    writeData(file, uri);
	}

	/**
	 * Writes the meta data from a {@link JGraphEditorFile} to a file with
	 * the given URI.
	 *
	 * @param file The file whose meta data to store.
	 * @param uri The destination URI.
	 * @throws MalformedURLException If the URI represents an malformed URL.
	 * @throws IOException If an error occurs while writing the meta data.
	 */
	private void writeMetaData(JGraphEditorFile file, String uri)
	        throws MalformedURLException, IOException {
		String ext = JGraphPersistenceManagerFactory.getFileExtension(uri);
		String currentURI = uri.substring(0, uri.indexOf(ext))
		    + JGraphpadBeanXMLPersistenceManager.TYPE_JAVA_BEAN_XML;
		super.writeObject(file, currentURI);
	}

	/**
	 * Writes the data of a {@link JGraphEditorFile} to the given URI.
	 *
	 * @param file The file.
	 * @param uri The destination URI.
	 * @throws IOException If an error occurs while writing the file.
	 */
	private void writeData(JGraphEditorFile file, String uri)
	        throws IOException {
		try {
			TransformerHandler handler = createTransformerHandler(uri);
		    handler.startDocument();

		    AttributesImpl atts = new AttributesImpl();
		    handler.startElement("", "", DOCUMENT, atts);

		    JGraphpadFile jGraphpadFile = (JGraphpadFile) file;
		    Enumeration<?> children = jGraphpadFile.breadthFirstEnumeration();

		    while (children.hasMoreElements()) {
			    Object child = children.nextElement();
			    if (child instanceof ArchPadDiagram) {
			    	ArchPadDiagram diagram = (ArchPadDiagram) child;
			    	diagram.setMembersAttributeInRegionElementRoots();

			    	handler.startElement("", "", MODEL, atts);

				    List<ElementGraphCell> cells = getElementGraphCells(diagram);

              		GraphLayoutCache cache = diagram.getGraphLayoutCache();
            		DefaultGraphModel model =
            			(DefaultGraphModel) cache.getModel();

            		// store the cells
				    for (ElementGraphCell cell : cells) {
                		if (Utils.isComponent(cell)) {
                		    setTargetsAttribute(cell, model);
                		}
                        Element element = Utils.getCellElement(cell);
    					element.writeToXMLViaSAX(handler);
				    }

				    handler.endElement("", "", MODEL);
			    }
            }

		    handler.endElement("", "", DOCUMENT);
    		handler.endDocument();

		} catch (TransformerConfigurationException e) {
            throw new IOException(e);
		} catch (SAXException e) {
			throw new IOException(e);
		}
	}

	/**
	 * Sets the {@link ElementTreeNode#ATTRIBUTE_NEIGHBORS} attribute of the
	 * root {@link ElementTreeNode}in the {@link Element} of a cell.
	 *
	 * @param cell The cell.
	 * @param model The model.
	 */
	private void setTargetsAttribute(DefaultGraphCell cell,
			DefaultGraphModel model) {
		Object[] outgoingEdges = DefaultGraphModel.getEdges(model, cell, false);
		StringBuilder builder = new StringBuilder();

		for (Object object : outgoingEdges) {
			DefaultEdge edge = (DefaultEdge) object;
			DefaultPort targetChild = (DefaultPort) edge.getTarget();
			DefaultGraphCell target =
				(DefaultGraphCell) targetChild.getParent();
			ElementTreeNode root = getModelRoot(target);
			builder.append(root.getAttribute(ElementTreeNode.ATTRIBUTE_ID));
			builder.append(',');
		}

		if (outgoingEdges.length > 0) {
			int length = builder.length();
			builder.delete(length - 1, length);

			ElementTreeNode root = getModelRoot(cell);
		    root.setAttribute(ElementTreeNode.ATTRIBUTE_NEIGHBORS,
		    	builder.toString());
		}
	}

	/**
	 * Returns the root {@link ElementTreeNode} on the
	 * {@link Element} contained in a graph cell or edge.
	 *
	 * @param cell The cell or edge.
	 * @return The root node.
	 */
	private ElementTreeNode getModelRoot(DefaultGraphCell cell) {
		Element model = (Element) cell.getUserObject();
		if (model != null) {
			return (ElementTreeNode) model.getRoot();
	    }
		return null;
	}

	/**
	 * Creates and configures a {@link TransformerHandler} used for
	 * persisting the XML file to a given URI using SAX.
	 *
	 * @param uri The destination URI.
	 * @return The handler.
	 * @throws TransformerConfigurationException If the handler cannot be
	 *         created.
	 * @throws IOException n If an {@link OutputStream} cannot be created.
	 */
	private TransformerHandler createTransformerHandler(String uri)
	        throws TransformerConfigurationException, IOException {
		SAXTransformerFactory transformerFactory =
		    (SAXTransformerFactory) SAXTransformerFactory.newInstance();
	    TransformerHandler handler =
	    	transformerFactory.newTransformerHandler();

	    Transformer transformer = handler.getTransformer();
	    transformer.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
	    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
	    handler.setResult(new StreamResult(getOutputStream(uri)));

	    return handler;
	}

	/**
	 * Loads the XSD schema for XML files that store an ArchPad model.
	 *
	 * @return The {@link Schema}.
	 * @throws SAXException If an error occurs while instantiating the schema.
	 */
    private Schema loadSchema() throws SAXException {
        String language = XMLConstants.W3C_XML_SCHEMA_NS_URI;
        SchemaFactory factory = SchemaFactory.newInstance(language);

        try {
        	InputStream in = getInputStream(mSchemaURI);
        	return factory.newSchema(new StreamSource(in));	
        } catch (IOException ioe) {
        	// just log this and keep going - schema is not crucial
        	System.out.println(ioe);
        	return null;
        }
    }

    /**
     * Returns the {@link ElementGraphCell}s of a given diagram.
     *
     * @param diagram The diagram.
     * @return The cells.
     */
	private List<ElementGraphCell> getElementGraphCells(
			JGraphpadDiagram diagram) {
		 GraphLayoutCache cache = diagram.getGraphLayoutCache();
		 JGraphpadGraphModel model = (JGraphpadGraphModel) cache.getModel();

		 List<?> roots = model.getRoots();
		 List<?> cells = JGraphpadGraphModel.getDescendants(model,
			roots.toArray());

		 List<ElementGraphCell> eCells = new ArrayList<ElementGraphCell>();
		 for (Object cell : cells) {
			 if (cell instanceof ElementGraphCell) {
				 eCells.add((ElementGraphCell) cell);
			 }
		 }

		 return eCells;			
	}

    /**
     * Handler for SAX events. This class is responsible for construction
     * of the {@link JGraphEditorFile} instance being read from a file as
     * well as error handling.
     */
	class SAXParserHandler extends DefaultHandler {

		private static final String TYPE_FATAL_ERROR = "Fatal error";

		private static final String TYPE_ERROR = "Error";

		private static final String TYPE_WARNING = "Warning";

        private Stack<ElementTreeNode> mNodeStack =
        	new Stack<ElementTreeNode>();

        private Map<String, ElementGraphCell> mIdToCellMap =
        	new HashMap<String, ElementGraphCell>();

        private List<ElementGraphCell> mCurrentDiagramCells;

        private Iterator<ArchPadDiagram> mDiagramIterator;
        
        private ArchPadDiagram mCurrentDiagram;

        private Iterator<ElementGraphCell> mCellIterator;

        private ElementGraphCell mCurrentCell;

        /**
         * Creates a new instance for populating the data of the
         * <code>diagrams</code> that represent the meta data.
         *
         * @param diagrams The diagrams.
         */
        SAXParserHandler(List<ArchPadDiagram> diagrams) {
            mDiagramIterator = diagrams.iterator();
        }

        @Override
		public void startElement(String uri, String localName, String qName,
        		Attributes attrs) throws SAXException {

        	if (DOCUMENT.equals(qName)) {
        		/* do nothing */
        	} else if (MODEL.equals(qName)) {
        		gotoNextDiagram();
        		installGraphRefreshingListener();
        		putDiagramCellsInIdToCellMap();
        	} else {
        	    if (isArchitecturalElement(qName)) {
        		    gotoNextVertex();
        	    }

        	    ElementTreeNode node = createElementTreeNode(qName, attrs);

                if (REGION.equals(qName)) {
                	reconstructRegion(node);
                }       	    

        	    if (!mNodeStack.isEmpty()) {
            		mNodeStack.peek().add(node);
            	}

            	mNodeStack.push(node);
        	}
        }

        /**
         * Installs a listener for detecting the first time the graph for the
         * current <code>diagram</code> is focused and in case of that event
         * refreshes the graph. The installed listener is executed only once
         * after which it unregisters itself.
         *
         *  Node: This is tricky and required since painting of cell's region
         *      tokens in {@link ArchPadVertexRenderer#paintRegions} needs the
         *      current diagram to fetch regions' colors. However, the graph
         *      for the new diagram pane (for the <code>diagram</code> we are
         *      loading) is focused after it has been painted and the current
         *      diagram is the one of the focused graph.
         */
        private void installGraphRefreshingListener() {
        	final ArchPadDiagram fDiagram = mCurrentDiagram;
        	final JGraphpadFocusManager fManager =
	    	    JGraphpadFocusManager.getCurrentGraphFocusManager();

    		fManager.addPropertyChangeListener(new PropertyChangeListener() {
    	        public void propertyChange(PropertyChangeEvent e) {
		            Object newValue = e.getNewValue();
		            if (newValue instanceof JGraph) {
    	    	        JGraph graph = (JGraph) e.getNewValue();
		                if (graph.getGraphLayoutCache()
		                		.equals(fDiagram.getGraphLayoutCache())) {
    	    	            graph.refresh();
		                    fManager.removePropertyChangeListener(this);
		                }
		            }
    	        }
            });
        }

        /**
         * Goes to the next diagram.
         *
         * @throws IllegalArgumentException if there is no next diagram which
         *     shows a mismatch between the data and meta-data files.
         */
        private void gotoNextDiagram() {
        	if (mDiagramIterator.hasNext()) {
    		    mCurrentDiagram = mDiagramIterator.next();
    		    mCellIterator = null;
    		} else {
    		    throw new IllegalArgumentException(
    		    	MESSAGE_DATA_AND_METADATA_MISMATCH);	
    		}
        }

        /**
         * Goes to the next vertex to be populated with meta-data.
         *
         * @throws IllegalArgumentException if there is no next vertex which
         *     shows a mismatch between the data and meta-data files.
         */
        private void gotoNextVertex() {

        	if (mCellIterator == null) {
    		    mCellIterator = mCurrentDiagramCells.iterator();
        	}

        	if (mCellIterator.hasNext()) {
		    	mCurrentCell = mCellIterator.next();
        	} else {
        	    throw new IllegalArgumentException(
        	    	MESSAGE_DATA_AND_METADATA_MISMATCH);
        	}
        }

        /**
         * Returns if the name of an node represents an architectural element.
         *
         * @param name The name.
         * @return True if the name is an architectural element,
         *     false otherwise.
         */
        private boolean isArchitecturalElement(String name) {
        	return COMPONENT.equals(name)
        	    || CONNECTOR.equals(name)
        	    || REGION.equals(name);
        }

        /**
         * Creates an {@link ElementTreeNode} and populates its attributes.
         *
         * @param name The node name.
         * @param attrs The attributes to set.
         * @return A new populated node.
         */
        private ElementTreeNode createElementTreeNode(String name,
        		Attributes attrs) {

        	ElementDeclaration typeDeclaration =
        		Element.getTypeDeclaration(name);

        	ElementTreeNode node = new ElementTreeNode(typeDeclaration);
    	    for (int i = 0, count = attrs.getLength(); i < count; i++) {
    		    node.setAttribute(attrs.getLocalName(i),
    			    	attrs.getValue(i));
    	    }

    	    return node;
        }
 
        /**
         * Populates the id to cell map with the cells.
         */
        private void putDiagramCellsInIdToCellMap() {

        	mIdToCellMap.clear();

    		mCurrentDiagramCells = getElementGraphCells(mCurrentDiagram);
	    	for (ElementGraphCell cell : mCurrentDiagramCells) {
	    		String key = cell.getAttribute(ElementGraphCell.ATTRIBUTE_ID);
	    		mIdToCellMap.put(key, cell);
	    	}
        }

        /**
         * Reconstructs a region given its root node.
         *
         * @param node The region root node.
         */
        private void reconstructRegion(ElementTreeNode node) {
            String value = node.getAttribute(ElementTreeNode.ATTRIBUTE_MEMBERS);

		    String[] ids = value.split(",");
		    int length = ids.length;

		    ElementGraphCell[] members = new ElementGraphCell[length];
		    for (int i = 0; i < length; i++) {
		        members[i] = mIdToCellMap.get(ids[i]);
		    }

		    mCurrentDiagram.getRegionManager().createRegion(mCurrentCell,
		    	members);
        }

        @Override
        public void endElement(String uri, String localName, String qName)
                throws SAXException {
        	if (!DOCUMENT.equals(qName) && !MODEL.equals(qName)) {
        		ElementTreeNode root = mNodeStack.pop();
        		if (isArchitecturalElement(qName)) {
            	    Element userObject = new Element(root);
            	    mCurrentCell.setUserObject(userObject);
        		}
        	}
        }

        @Override
        public void characters(char ch[], int start, int length)
                throws SAXException {
        	if (!mNodeStack.isEmpty()) {
        		String value = new String(ch, start, length).trim();
        		if (!value.isEmpty() && value.charAt(0) != '\n') {
        		    mNodeStack.peek().setValue(value);
        		}
        	}
        }

        @Override
		public void error(SAXParseException e) throws SAXException {
			printException(e, TYPE_ERROR);
		}

		@Override
		public void fatalError(SAXParseException e) throws SAXException {
			printException(e, TYPE_FATAL_ERROR);
		}

		@Override
		public void warning(SAXParseException e) throws SAXException {
			printException(e, TYPE_WARNING);
	    }

        private void printException(SAXParseException e, String type) {
	    	System.out.println("   Type: " + type);
	        System.out.println("   Line number: " + e.getLineNumber());
	        System.out.println("   Column number: " + e.getColumnNumber());
	        System.out.println("   Message: " + e.getMessage());
	        System.out.println();
	    }
    }

	/**
	 * Factory method for {@link ArchPadXMLPersistenceManager}s.
	 */
	public static class FactoryMethod
	        extends JGraphPersistenceManagerFactoryMethod<JGraphEditorFile> {

		/**
		 * Creates a new instance with <code>properties</code> used for
		 * configuring the produced {@link ArchPadXMLPersistenceManager}.
		 *
		 * @param properties The properties.
		 */
		public FactoryMethod(Properties properties) {
			super(SUPPORTED_FILE_TYPES, properties);
		}

		public JGraphpadAbstractPersistenceManager<JGraphEditorFile>
		        createPersistenceManager(Properties properties) {
			return new ArchPadXMLPersistenceManager(properties);
		}
	}
}
